/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.serotonin.modbus4j.test;

import jssc.SerialPort;

import java.io.IOException;
import java.io.InputStream;

/**
 * Class that wraps a {@link SerialPort} to provide {@link InputStream}
 * functionality. This stream also provides support for performing blocking
 * reads with timeouts.
 * <br>
 * It is instantiated by passing the constructor a {@link SerialPort} instance.
 * Do not create multiple streams for the same serial port unless you implement
 * your own synchronization.
 *
 * @author Charles Hache <chalz@member.fsf.org>
 *
 * Attribution: https://github.com/therealchalz/java-simple-serial-connector
 *
 */
public class SerialInputStream extends InputStream {

  private SerialPort serialPort;
  private int defaultTimeout = 0;

  /**
   * Instantiates a SerialInputStream for the given {@link SerialPort} Do not
   * create multiple streams for the same serial port unless you implement
   * your own synchronization.
   *
   * @param sp The serial port to stream.
   */
  public SerialInputStream(SerialPort sp) {
    serialPort = sp;
  }

  /**
   * Set the default timeout (ms) of this SerialInputStream. This affects
   * subsequent calls to {@link #read()}, {@link #blockingRead(int[])}, and
   * {@link #blockingRead(int[], int, int)} The default timeout can be 'unset'
   * by setting it to 0.
   *
   * @param time The timeout in milliseconds.
   */
  public void setTimeout(int time) {
    defaultTimeout = time;
  }

  /**
   * Reads the next byte from the port. If the timeout of this stream has been
   * set, then this method blocks until data is available or until the timeout
   * has been hit. If the timeout is not set or has been set to 0, then this
   * method blocks indefinitely.
   */
  @Override
  public int read() throws IOException {
    return read(defaultTimeout);
  }

  /**
   * The same contract as {@link #read()}, except overrides this stream's
   * default timeout with the given timeout in milliseconds.
   *
   * @param timeout The timeout in milliseconds.
   * @return The read byte.
   * @throws IOException On serial port error or timeout
   */
  public int read(int timeout) throws IOException {
    byte[] buf = new byte[1];
    try {
      if (timeout > 0) {
        buf = serialPort.readBytes(1, timeout);
      } else {
        buf = serialPort.readBytes(1);
      }
      return buf[0];
    } catch (Exception e) {
      throw new IOException(e);
    }
  }

  /**
   * Non-blocking read of up to buf.length bytes from the stream. This call
   * behaves as read(buf, 0, buf.length) would.
   *
   * @param buf The buffer to fill.
   * @return The number of bytes read, which can be 0.
   * @throws IOException on error.
   */
  @Override
  public int read(byte[] buf) throws IOException {
    return read(buf, 0, buf.length);
  }

  /**
   * Non-blocking read of up to length bytes from the stream. This method
   * returns what is immediately available in the input buffer.
   *
   * @param buf The buffer to fill.
   * @param offset The offset into the buffer to start copying data.
   * @param length The maximum number of bytes to read.
   * @return The actual number of bytes read, which can be 0.
   * @throws IOException on error.
   */
  @Override
  public int read(byte[] buf, int offset, int length) throws IOException {

    if (buf.length < offset + length) {
      length = buf.length - offset;
    }

    int available = this.available();

    if (available > length) {
      available = length;
    }

    try {
      byte[] readBuf = serialPort.readBytes(available);
      System.arraycopy(readBuf, 0, buf, offset, readBuf.length);
      return readBuf.length;
    } catch (Exception e) {
      throw new IOException(e);
    }
  }

  /**
   * Blocks until buf.length bytes are read, an error occurs, or the default
   * timeout is hit (if specified). This behaves as blockingRead(buf, 0,
   * buf.length) would.
   *
   * @param buf The buffer to fill with data.
   * @return The number of bytes read.
   * @throws IOException On error or timeout.
   */
  public int blockingRead(byte[] buf) throws IOException {
    return blockingRead(buf, 0, buf.length, defaultTimeout);
  }

  /**
   * The same contract as {@link #blockingRead(byte[])} except overrides this
   * stream's default timeout with the given one.
   *
   * @param buf The buffer to fill.
   * @param timeout The timeout in milliseconds.
   * @return The number of bytes read.
   * @throws IOException On error or timeout.
   */
  public int blockingRead(byte[] buf, int timeout) throws IOException {
    return blockingRead(buf, 0, buf.length, timeout);
  }

  /**
   * Blocks until length bytes are read, an error occurs, or the default
   * timeout is hit (if specified). Saves the data into the given buffer at
   * the specified offset. If the stream's timeout is not set, behaves as
   * {@link #read(byte[], int, int)} would.
   *
   * @param buf The buffer to fill.
   * @param offset The offset in buffer to save the data.
   * @param length The number of bytes to read.
   * @return the number of bytes read.
   * @throws IOException on error or timeout.
   */
  public int blockingRead(byte[] buf, int offset, int length) throws IOException {
    return blockingRead(buf, offset, length, defaultTimeout);
  }

  /**
   * The same contract as {@link #blockingRead(byte[], int, int)} except
   * overrides this stream's default timeout with the given one.
   *
   * @param buf The buffer to fill.
   * @param offset Offset in the buffer to start saving data.
   * @param length The number of bytes to read.
   * @param timeout The timeout in milliseconds.
   * @return The number of bytes read.
   * @throws IOException On error or timeout.
   */
  public int blockingRead(byte[] buf, int offset, int length, int timeout) throws IOException {
    if (buf.length < offset + length) {
      throw new IOException("Not enough buffer space for serial data");
    }

    if (timeout < 1) {
      return read(buf, offset, length);
    }

    try {
      byte[] readBuf = serialPort.readBytes(length, timeout);
      System.arraycopy(readBuf, 0, buf, offset, length);
      return readBuf.length;
    } catch (Exception e) {
      throw new IOException(e);
    }
  }

  @Override
  public int available() throws IOException {
    int ret;
    try {
      ret = serialPort.getInputBufferBytesCount();
      if (ret >= 0) {
        return ret;
      }
      throw new IOException("Error checking available bytes from the serial port.");
    } catch (Exception e) {
      throw new IOException("Error checking available bytes from the serial port.");
    }
  }

}